﻿using System;
using System.Runtime.CompilerServices;
using Aeon.Emulator.RuntimeExceptions;

namespace Aeon.Emulator.Memory
{
    /// <summary>
    /// Provides read/write access to emulated physical memory with paging.
    /// </summary>
    internal sealed class PagedMemoryAccessor : MemoryAccessor, IDisposable
    {
        #region Constructors
        /// <summary>
        /// Initializes a new instance of the PagedMemoryAccessor class.
        /// </summary>
        /// <param name="rawView">Emulated physicaal memory.</param>
        public PagedMemoryAccessor(IntPtr rawView)
            : base(rawView)
        {
            unsafe
            {
                this.nativeMemory = new NativeMemory(CacheSize * sizeof(uint));
                this.pageCache = (uint*)this.nativeMemory.Pointer.ToPointer();
            }
        }
        #endregion

        #region Public Properties
        /// <summary>
        /// Gets or sets the linear address of the page table directory.
        /// </summary>
        public uint DirectoryAddress
        {
            get
            {
                return this.directoryAddress; 
            }
            set
            {
                this.directoryAddress = value;
                this.FlushCache();
            }
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Reads a byte from emulated memory.
        /// </summary>
        /// <param name="address">Physical address of byte to read.</param>
        /// <returns>Byte at the specified address.</returns>
        public override byte GetByte(uint address)
        {
            address = GetPhysicalAddress(address, PageFaultCause.Read);

            if(address >= PhysicalMemory.VramAddress && address < PhysicalMemory.VramUpperBound)
                return video.GetVramByte(address - PhysicalMemory.VramAddress);

            unsafe
            {
                return RawView[address];
            }
        }
        /// <summary>
        /// Writes a byte to emulated memory.
        /// </summary>
        /// <param name="address">Physical address of byte to write.</param>
        /// <param name="value">Value to write to the specified address.</param>
        public override void SetByte(uint address, byte value)
        {
            address = GetPhysicalAddress(address, PageFaultCause.Write);

            if(address >= PhysicalMemory.VramAddress && address < PhysicalMemory.VramUpperBound)
            {
                video.SetVramByte(address - PhysicalMemory.VramAddress, value);
                return;
            }

            unsafe
            {
                RawView[address] = value;
            }
        }
        /// <summary>
        /// Reads an unsigned 16-bit integer from emulated memory.
        /// </summary>
        /// <param name="address">Physical address of unsigned 16-bit integer to read.</param>
        /// <returns>Unsigned 16-bit integer at the specified address.</returns>
        public override ushort GetUInt16(uint address)
        {
            uint baseAddress = GetPhysicalAddress(address, PageFaultCause.Read);

            if(baseAddress >= PhysicalMemory.VramAddress && baseAddress < PhysicalMemory.VramUpperBound)
                return this.video.GetVramWord(baseAddress - PhysicalMemory.VramAddress);

            unsafe
            {
                if((baseAddress & 0xFFFu) != 0xFFEu)
                {
                    return *(ushort*)(RawView + baseAddress);
                }
                else
                {
                    byte* buf = stackalloc byte[2];
                    buf[0] = RawView[baseAddress];
                    buf[1] = RawView[GetPhysicalAddress(address + 1u, PageFaultCause.Read)];
                    return *(ushort*)buf;
                }
            }
        }
        /// <summary>
        /// Writes an unsigned 16-bit integer to emulated memory.
        /// </summary>
        /// <param name="address">Physical address of unsigned 16-bit integer to write.</param>
        /// <param name="value">Value to write to the specified address.</param>
        public override void SetUInt16(uint address, ushort value)
        {
            uint baseAddress = GetPhysicalAddress(address, PageFaultCause.Write);

            if(baseAddress >= PhysicalMemory.VramAddress && baseAddress < PhysicalMemory.VramUpperBound)
            {
                this.video.SetVramWord(address - PhysicalMemory.VramAddress, value);
                return;
            }

            unsafe
            {
                if((baseAddress & 0xFFFu) != 0xFFEu)
                {
                    *(ushort*)(RawView + baseAddress) = value;
                }
                else
                {
                    byte* buf = (byte*)&value;
                    RawView[baseAddress] = buf[0];
                    RawView[GetPhysicalAddress(address + 1u, PageFaultCause.Write)] = buf[1];
                }
            }
        }
        /// <summary>
        /// Reads an unsigned 32-bit integer from emulated memory.
        /// </summary>
        /// <param name="address">Physical address of unsigned 32-bit integer to read.</param>
        /// <returns>Unsigned 32-bit integer at the specified address.</returns>
        public override uint GetUInt32(uint address)
        {
            uint baseAddress = GetPhysicalAddress(address, PageFaultCause.Read);

            if(baseAddress >= PhysicalMemory.VramAddress && baseAddress < PhysicalMemory.VramUpperBound)
                return this.video.GetVramDWord(baseAddress - PhysicalMemory.VramAddress);

            unsafe
            {
                if((baseAddress & 0xFFFu) < 4096u - 4u)
                {
                    return *(uint*)(RawView + baseAddress);
                }
                else
                {
                    byte* buf = stackalloc byte[4];
                    buf[0] = RawView[baseAddress];
                    buf[1] = RawView[GetPhysicalAddress(address + 1u, PageFaultCause.Read)];
                    buf[2] = RawView[GetPhysicalAddress(address + 2u, PageFaultCause.Read)];
                    buf[3] = RawView[GetPhysicalAddress(address + 3u, PageFaultCause.Read)];
                    return *(uint*)buf;
                }
            }
        }
        /// <summary>
        /// Writes an unsigned 32-bit integer to emulated memory.
        /// </summary>
        /// <param name="address">Physical address of unsigned 32-bit integer to write.</param>
        /// <param name="value">Value to write to the specified address.</param>
        public override void SetUInt32(uint address, uint value)
        {
            uint baseAddress = GetPhysicalAddress(address, PageFaultCause.Write);

            if(baseAddress >= PhysicalMemory.VramAddress && baseAddress < PhysicalMemory.VramUpperBound)
            {
                this.video.SetVramDWord(address - PhysicalMemory.VramAddress, value);
                return;
            }

            unsafe
            {
                if((baseAddress & 0xFFFu) < 4096u - 4u)
                {
                    *(uint*)(RawView + baseAddress) = value;
                }
                else
                {
                    byte* buf = (byte*)&value;
                    RawView[baseAddress] = buf[0];
                    RawView[GetPhysicalAddress(address + 1u, PageFaultCause.Write)] = buf[1];
                    RawView[GetPhysicalAddress(address + 2u, PageFaultCause.Write)] = buf[2];
                    RawView[GetPhysicalAddress(address + 3u, PageFaultCause.Write)] = buf[3];
                }
            }
        }
        /// <summary>
        /// Reads an unsigned 64-bit integer from emulated memory.
        /// </summary>
        /// <param name="address">Physical address of unsigned 64-bit integer to read.</param>
        /// <returns>Unsigned 64-bit integer at the specified address.</returns>
        public override ulong GetUInt64(uint address)
        {
            uint baseAddress = GetPhysicalAddress(address, PageFaultCause.Read);

            unsafe
            {
                if((baseAddress & 0xFFFu) < 4096u - 8u)
                {
                    return *(ulong*)(RawView + baseAddress);
                }
                else
                {
                    byte* buf = stackalloc byte[8];
                    buf[0] = RawView[baseAddress];
                    buf[1] = RawView[GetPhysicalAddress(address + 1u, PageFaultCause.Read)];
                    buf[2] = RawView[GetPhysicalAddress(address + 2u, PageFaultCause.Read)];
                    buf[3] = RawView[GetPhysicalAddress(address + 3u, PageFaultCause.Read)];
                    buf[4] = RawView[GetPhysicalAddress(address + 4u, PageFaultCause.Read)];
                    buf[5] = RawView[GetPhysicalAddress(address + 5u, PageFaultCause.Read)];
                    buf[6] = RawView[GetPhysicalAddress(address + 6u, PageFaultCause.Read)];
                    buf[7] = RawView[GetPhysicalAddress(address + 7u, PageFaultCause.Read)];
                    return *(ulong*)buf;
                }
            }
        }
        /// <summary>
        /// Writes an unsigned 64-bit integer to emulated memory.
        /// </summary>
        /// <param name="address">Physical address of unsigned 64-bit integer to write.</param>
        /// <param name="value">Value to write to the specified address.</param>
        public override void SetUInt64(uint address, ulong value)
        {
            uint baseAddress = GetPhysicalAddress(address, PageFaultCause.Write);

            unsafe
            {
                if((baseAddress & 0xFFFu) < 4096u - 8u)
                {
                    *(ulong*)(RawView + baseAddress) = value;
                }
                else
                {
                    byte* buf = (byte*)&value;
                    RawView[baseAddress] = buf[0];
                    RawView[GetPhysicalAddress(address + 1u, PageFaultCause.Write)] = buf[1];
                    RawView[GetPhysicalAddress(address + 2u, PageFaultCause.Write)] = buf[2];
                    RawView[GetPhysicalAddress(address + 3u, PageFaultCause.Write)] = buf[3];
                    RawView[GetPhysicalAddress(address + 4u, PageFaultCause.Write)] = buf[4];
                    RawView[GetPhysicalAddress(address + 5u, PageFaultCause.Write)] = buf[5];
                    RawView[GetPhysicalAddress(address + 6u, PageFaultCause.Write)] = buf[6];
                    RawView[GetPhysicalAddress(address + 7u, PageFaultCause.Write)] = buf[7];
                }
            }
        }
        /// <summary>
        /// Returns a System.Single value read from an address in emulated memory.
        /// </summary>
        /// <param name="address">Address of value to read.</param>
        /// <returns>32-bit System.Single value read from the specified address.</returns>
        public override float GetReal32(uint address)
        {
            uint baseAddress = GetPhysicalAddress(address, PageFaultCause.Read);

            unsafe
            {
                if((baseAddress & 0xFFFu) < 4096u - 4u)
                {
                    return *(float*)(RawView + baseAddress);
                }
                else
                {
                    byte* buf = stackalloc byte[4];
                    buf[0] = RawView[baseAddress];
                    buf[1] = RawView[GetPhysicalAddress(address + 1u, PageFaultCause.Read)];
                    buf[2] = RawView[GetPhysicalAddress(address + 2u, PageFaultCause.Read)];
                    buf[3] = RawView[GetPhysicalAddress(address + 3u, PageFaultCause.Read)];
                    return *(float*)buf;
                }
            }
        }
        /// <summary>
        /// Writes a System.Single value to an address in emulated memory.
        /// </summary>
        /// <param name="address">Address where value will be written.</param>
        /// <param name="value">32-bit System.Single value to write at the specified address.</param>
        public override void SetReal32(uint address, float value)
        {
            uint baseAddress = GetPhysicalAddress(address, PageFaultCause.Write);

            unsafe
            {
                if((baseAddress & 0xFFFu) < 4096u - 4u)
                {
                    *(float*)(RawView + baseAddress) = value;
                }
                else
                {
                    byte* buf = (byte*)&value;
                    RawView[baseAddress] = buf[0];
                    RawView[GetPhysicalAddress(address + 1u, PageFaultCause.Write)] = buf[1];
                    RawView[GetPhysicalAddress(address + 2u, PageFaultCause.Write)] = buf[2];
                    RawView[GetPhysicalAddress(address + 3u, PageFaultCause.Write)] = buf[3];
                }
            }
        }
        /// <summary>
        /// Returns a System.Double value read from an address in emulated memory.
        /// </summary>
        /// <param name="address">Address of value to read.</param>
        /// <returns>64-bit System.Double value read from the specified address.</returns>
        public override double GetReal64(uint address)
        {
            uint baseAddress = GetPhysicalAddress(address, PageFaultCause.Read);

            unsafe
            {
                if((baseAddress & 0xFFFu) < 4096u - 8u)
                {
                    return *(double*)(RawView + baseAddress);
                }
                else
                {
                    byte* buf = stackalloc byte[8];
                    buf[0] = RawView[baseAddress];
                    buf[1] = RawView[GetPhysicalAddress(address + 1u, PageFaultCause.Read)];
                    buf[2] = RawView[GetPhysicalAddress(address + 2u, PageFaultCause.Read)];
                    buf[3] = RawView[GetPhysicalAddress(address + 3u, PageFaultCause.Read)];
                    buf[4] = RawView[GetPhysicalAddress(address + 4u, PageFaultCause.Read)];
                    buf[5] = RawView[GetPhysicalAddress(address + 5u, PageFaultCause.Read)];
                    buf[6] = RawView[GetPhysicalAddress(address + 6u, PageFaultCause.Read)];
                    buf[7] = RawView[GetPhysicalAddress(address + 7u, PageFaultCause.Read)];
                    return *(double*)buf;
                }
            }
        }
        /// <summary>
        /// Writes a System.Double value to an address in emulated memory.
        /// </summary>
        /// <param name="address">Address where value will be written.</param>
        /// <param name="value">64-bit System.Double value to write at the specified address.</param>
        public override void SetReal64(uint address, double value)
        {
            uint baseAddress = GetPhysicalAddress(address, PageFaultCause.Write);

            unsafe
            {
                if((baseAddress & 0xFFFu) < 4096u - 8u)
                {
                    *(double*)(RawView + baseAddress) = value;
                }
                else
                {
                    byte* buf = (byte*)&value;
                    RawView[baseAddress] = buf[0];
                    RawView[GetPhysicalAddress(address + 1u, PageFaultCause.Write)] = buf[1];
                    RawView[GetPhysicalAddress(address + 2u, PageFaultCause.Write)] = buf[2];
                    RawView[GetPhysicalAddress(address + 3u, PageFaultCause.Write)] = buf[3];
                    RawView[GetPhysicalAddress(address + 4u, PageFaultCause.Write)] = buf[4];
                    RawView[GetPhysicalAddress(address + 5u, PageFaultCause.Write)] = buf[5];
                    RawView[GetPhysicalAddress(address + 6u, PageFaultCause.Write)] = buf[6];
                    RawView[GetPhysicalAddress(address + 7u, PageFaultCause.Write)] = buf[7];
                }
            }
        }
        /// <summary>
        /// Returns a Real10 value read from an address in emulated memory.
        /// </summary>
        /// <param name="address">Address of value to read.</param>
        /// <returns>80-bit Real10 value read from the specified address.</returns>
        public override Real10 GetReal80(uint address)
        {
            uint baseAddress = GetPhysicalAddress(address, PageFaultCause.Read);

            unsafe
            {
                if((baseAddress & 0xFFFu) < 4096u - 10u)
                {
                    return *(Real10*)(RawView + baseAddress);
                }
                else
                {
                    byte* buf = stackalloc byte[10];
                    buf[0] = RawView[baseAddress];
                    buf[1] = RawView[GetPhysicalAddress(address + 1u, PageFaultCause.Read)];
                    buf[2] = RawView[GetPhysicalAddress(address + 2u, PageFaultCause.Read)];
                    buf[3] = RawView[GetPhysicalAddress(address + 3u, PageFaultCause.Read)];
                    buf[4] = RawView[GetPhysicalAddress(address + 4u, PageFaultCause.Read)];
                    buf[5] = RawView[GetPhysicalAddress(address + 5u, PageFaultCause.Read)];
                    buf[6] = RawView[GetPhysicalAddress(address + 6u, PageFaultCause.Read)];
                    buf[7] = RawView[GetPhysicalAddress(address + 7u, PageFaultCause.Read)];
                    buf[8] = RawView[GetPhysicalAddress(address + 8u, PageFaultCause.Read)];
                    buf[9] = RawView[GetPhysicalAddress(address + 9u, PageFaultCause.Read)];
                    return *(Real10*)buf;
                }
            }
        }
        /// <summary>
        /// Writes a Real10 value to an address in emulated memory.
        /// </summary>
        /// <param name="address">Address where value will be written.</param>
        /// <param name="value">80-bit Real10 value to write at the specified address.</param>
        public override void SetReal80(uint address, Real10 value)
        {
            uint baseAddress = GetPhysicalAddress(address, PageFaultCause.Write);

            unsafe
            {
                if((baseAddress & 0xFFFu) < 4096u - 10u)
                {
                    *(Real10*)(RawView + baseAddress) = value;
                }
                else
                {
                    byte* buf = (byte*)&value;
                    RawView[baseAddress] = buf[0];
                    RawView[GetPhysicalAddress(address + 1u, PageFaultCause.Write)] = buf[1];
                    RawView[GetPhysicalAddress(address + 2u, PageFaultCause.Write)] = buf[2];
                    RawView[GetPhysicalAddress(address + 3u, PageFaultCause.Write)] = buf[3];
                    RawView[GetPhysicalAddress(address + 4u, PageFaultCause.Write)] = buf[4];
                    RawView[GetPhysicalAddress(address + 5u, PageFaultCause.Write)] = buf[5];
                    RawView[GetPhysicalAddress(address + 6u, PageFaultCause.Write)] = buf[6];
                    RawView[GetPhysicalAddress(address + 7u, PageFaultCause.Write)] = buf[7];
                    RawView[GetPhysicalAddress(address + 8u, PageFaultCause.Write)] = buf[8];
                    RawView[GetPhysicalAddress(address + 9u, PageFaultCause.Write)] = buf[9];
                }
            }
        }
        /// <summary>
        /// Reads 16 bytes from emulated memory into a buffer.
        /// </summary>
        /// <param name="address">Address where bytes will be read from.</param>
        /// <param name="buffer">Buffer into which bytes will be written.</param>
        public override unsafe void FetchInstruction(uint address, byte* buffer)
        {
            uint baseAddress = GetPhysicalAddress(address, PageFaultCause.InstructionFetch);

            if((address & 0xFFFu) < 4096 - 16)
            {
                *(ulong*)buffer = *(ulong*)(RawView + baseAddress);
                ((ulong*)buffer)[1] = ((ulong*)(RawView + baseAddress))[1];
            }
            else
            {
                for(uint i = 0; i < 16; i++)
                {
                    buffer[i] = RawView[baseAddress];
                    baseAddress = GetPhysicalAddress(address + i + 1u, PageFaultCause.InstructionFetch);
                }
            }
        }
        /// <summary>
        /// Returns a pointer to a block of memory, making sure it is paged in.
        /// </summary>
        /// <param name="address">Logical address of block.</param>
        /// <param name="size">Number of bytes in block of memory.</param>
        /// <returns>Pointer to block of memory.</returns>
        public override unsafe void* GetSafePointer(uint address, uint size)
        {
            uint baseAddress = GetPhysicalAddress(address, PageFaultCause.Read);
            if((address & 0xFFFu) + size > 4096)
                GetPhysicalAddress(address + 4096u, PageFaultCause.Read);

            return RawView + baseAddress;
        }
        /// <summary>
        /// Flushes the page address cache.
        /// </summary>
        public void FlushCache()
        {
            this.nativeMemory.Clear();
        }
        /// <summary>
        /// Returns the physical address from a paged logical address.
        /// </summary>
        /// <param name="logicalAddress">Logical address to resolve.</param>
        /// <returns>Physical address of the supplied logical address.</returns>
        public uint GetPhysicalAddress(uint logicalAddress)
        {
            return GetPhysicalAddress(logicalAddress, PageFaultCause.Read);
        }
        /// <summary>
        /// Releases resources used by the PagedMemoryAccessor.
        /// </summary>
        public void Dispose()
        {
            this.nativeMemory.Dispose();
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Returns the physical address from a paged linear address.
        /// </summary>
        /// <param name="linearAddress">Paged linear address.</param>
        /// <param name="operation">Type of operation attempted in case of a page fault.</param>
        /// <returns>Physical address of the supplied linear address.</returns>
        private uint GetPhysicalAddress(uint linearAddress, PageFaultCause operation)
        {
            uint pageCacheIndex = linearAddress >> 12;

            unsafe
            {
                if(this.pageCache[pageCacheIndex] != 0)
                    return this.pageCache[pageCacheIndex] | (linearAddress & 0xFFFu);
            }

            uint baseAddress = linearAddress & 0xFFFFFC00u;

            var physicalAddress = GetPage(linearAddress, operation);

            unsafe
            {
                this.pageCache[pageCacheIndex] = physicalAddress;
            }

            return physicalAddress | (linearAddress & 0xFFFu);
        }
        /// <summary>
        /// Looks up a page's physical address.
        /// </summary>
        /// <param name="linearAddress">Paged linear address.</param>
        /// <param name="operation">Type of operation attempted in case of a page fault.</param>
        /// <returns>Physical address of the page.</returns>
        [MethodImpl(MethodImplOptions.NoInlining)]
        private uint GetPage(uint linearAddress, PageFaultCause operation)
        {
            uint page = (linearAddress >> 12) & 0x3FFu;
            uint dir = linearAddress >> 22;

            unsafe
            {
                uint* dirPtr = (uint*)(RawView + directoryAddress);
                if((dirPtr[dir] & PagePresent) == 0)
                    throw new PageFaultException(linearAddress, operation);

                uint pageAddress = dirPtr[dir] & 0xFFFFF000u;
                uint* pagePtr = (uint*)(RawView + pageAddress);
                if((pagePtr[page] & PagePresent) == 0)
                    throw new PageFaultException(linearAddress, operation);

                return pagePtr[page] & 0xFFFFF000u;
            }
        }
        #endregion

        #region Private Fields
        /// <summary>
        /// The linear address of the page table directory.
        /// </summary>
        private uint directoryAddress;
        /// <summary>
        /// Array of cached physical page addresses.
        /// </summary>
        private unsafe readonly uint* pageCache;
        /// <summary>
        /// Manages the page cache's memory allocation.
        /// </summary>
        private readonly NativeMemory nativeMemory;
        #endregion

        #region Private Constants
        /// <summary>
        /// Size of the page address cache in bytes.
        /// </summary>
        private const int CacheSize = 1 << 20;
        /// <summary>
        /// Bit in page table entry which indicates that a page is present.
        /// </summary>
        private const uint PagePresent = 1 << 0;
        #endregion
    }
}
